package it.tukano.odt;

import it.tukano.collections.HashMapExt;
import it.tukano.collections.LinkedListExt;
import it.tukano.collections.ListExt;
import it.tukano.collections.MapExt;
import it.tukano.fun.Function1;
import it.tukano.fun.Function3;
import it.tukano.fun.Tuple2;
import it.tukano.fun.VFunction2;
import it.tukano.io.FileExt;
import it.tukano.io.Zipper;
import it.tukano.jpgi.test.SvgContainer;
import it.tukano.lang.StringExt;
import it.tukano.log.Log;
import it.tukano.reflection.MethodExt;
import it.tukano.swingdeco.ImageIconExt;
import it.tukano.swingdeco.containers.JToolBarExt;
import it.tukano.swingdeco.controls.JButtonExt;
import it.tukano.swingdeco.draganddrop.DndUtilities;
import it.tukano.swingdeco.filechooser.JFileChooserExt;
import it.tukano.xml.XmlDoc;
import it.tukano.xml.XmlDoc.XmlNode;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;

/**
 * Usage: new OdtSvgForm(file).generateFields()<br/> file is a odt file. There
 * must be a svg file in the same directory with the same name (with .svg
 * extension). The svg file contains both the layout of the form and the
 * description of the fields.<br/> type=oplistitem | date | currency | varchar |
 * text | numeric | choice case=upper lower none size=a number (int) label=a
 * string index=a number (int) default=default value (string#string#string for a
 * choice item) optional=true | false (if true a check box will be added
 *
 * @author pgi
 */
public class OdtSvgForm extends JPanel implements OdtFieldContext {

    private static final long serialVersionUID = 0;
    private final FileExt file;
    private final FileExt svgFile;
    private final ListExt<OdtField> fields = new LinkedListExt<OdtField>();
    private final OdtFiller DROP_FILLER = new OdtFiller(fields);
    private final ListExt<Function1<OdtFiller, ?>> metaTransformers = new LinkedListExt<Function1<OdtFiller, ?>>();
    private final SvgContainer container;
    private final JButtonExt saveForm = new JButtonExt(new ImageIconExt("toolbarButtonGraphics.general", "Save24.gif")).onActionPerformed((Runnable) new MethodExt(this, "onSaveForm"));
    private final JButtonExt previewForm = new JButtonExt(new ImageIconExt("toolbarButtonGraphics.general", "PrintPreview24.gif")).onActionPerformed((Runnable) new MethodExt(this, "onPreviewForm"));
    private final JButtonExt newForm = new JButtonExt(new ImageIconExt("toolbarButtonGraphics.general", "New24.gif")).onActionPerformed((Runnable) new MethodExt(this, "onNewForm"));
    private final JButtonExt printForm = new JButtonExt(new ImageIconExt("toolbarButtonGraphics.general", "Print24.gif")).onActionPerformed((Runnable) new MethodExt(this, "onPrintForm"));
    private final JButtonExt quitForm = new JButtonExt(new ImageIconExt("toolbarButtonGraphics.navigation", "Back24.gif")).onActionPerformed((Runnable) new MethodExt(this, "onQuitForm"));
    private final JToolBarExt toolbar = new JToolBarExt().append(quitForm, newForm, previewForm, printForm, saveForm).floatable(false);
    private StringExt externalEditorPath;
    private List<Function1<OdtSvgForm, Boolean>> fileGenerationCheck = new LinkedList<Function1<OdtSvgForm, Boolean>>();
    private Function1<Properties, OdtField> defaultGenerator = new Function1<Properties, OdtField>() {

	public OdtField apply(Properties properties) {
	    String name = properties.getProperty("name");
	    OdtField field = new OdtField(name, properties);
	    if (field.hasEditor()) {
		field.getEditor().setName(name);
		field.getEditor().setBorder(null);
		Object defaultValue = properties.get("default");
		if (defaultValue != null) {
		    field.getEditor().setValue(new StringExt(defaultValue));
		}
	    }
	    return field;
	}
    };
    private Function1<OdtField, OdtField> defaultFieldInitializer = new Function1<OdtField, OdtField>() {

	public OdtField apply(OdtField p) {
	    return p;
	}
    };
    private Function1<String, String> defaultPostFieldTransformer = new Function1<String, String>() {

	public String apply(String xml) {
	    return xml;
	}
    };
    private Function3<OdtField, OdtFieldEditor, String, String> defaultEditorApplier = new Function3<OdtField, OdtFieldEditor, String, String>() {

	public String apply(OdtField field, OdtFieldEditor editor, String xml) {
	    return null;
	}
    };
    private final MapExt<String, Function1<Properties, OdtField>> generators = new HashMapExt<String, Function1<Properties, OdtField>>();
    private final MapExt<String, Function1<OdtField, OdtField>> initializers = new HashMapExt<String, Function1<OdtField, OdtField>>();
    private final ListExt<Runnable> onQuitFormCallbacks = new LinkedListExt<Runnable>();
    private final ListExt<Runnable> onSaveFormCallbacks = new LinkedListExt<Runnable>();

    /**
     * Instance initializer
     */
    public OdtSvgForm(FileExt odtFile) {
	super(new BorderLayout());
	this.file = odtFile;
	this.svgFile = odtFile.replaceExtension("svg");
	this.container = new SvgContainer(svgFile.toURI().toString());
	this.add(new JScrollPane(container));
	this.add(toolbar, BorderLayout.NORTH);
	DndUtilities.installFileDropHandler(toolbar, new Function1<FileExt, Boolean>() {

	    public Boolean apply(FileExt p) {
		return p.hasExtension("odt");
	    }
	}, new VFunction2<FileExt, Component>() {

	    @Override
	    public void doApply(FileExt a, Component b) {
		try {
		    DROP_FILLER.acquireDataFromOdtDocument(a);
		} catch (Exception ex) {
		    ex.printStackTrace(System.err);
		}
	    }
	});
    }

    /**
     * Esegue una funzione quando l'editor del campo in argomento invoca il suo
     * metodo onValueChange
     */
    public void onFieldAction(String fieldName, Runnable task) {
	OdtField field = getField(fieldName);
	if (field != null) {
	    OdtFieldEditor editor = field.getEditor();
	    if (editor != null) {
		editor.onValueChange(task);
	    } else {
		Logger.getLogger(OdtSvgForm.class.getName()).log(Level.INFO, "Attenzione, il campo {0} non ha un editor associato.", fieldName);
	    }
	} else {
	    Logger.getLogger(OdtSvgForm.class.getName()).log(Level.INFO, "Attenzione, il campo {0} non esiste in questo form.", fieldName);
	}
    }

    /**
     * Add a runnable that will be called every time this form is filled via a
     * odt drop.
     */
    public void addPostDropCallback(Function1<Map<String, StringExt>, ?> task) {
	DROP_FILLER.addPostDropTransform(task);
    }

    public void addFileGenerationCheck(Function1<OdtSvgForm, Boolean> fun) {
	this.fileGenerationCheck.add(fun);
    }

    public JToolBarExt getToolBar() {
	return toolbar;
    }

    public OdtSvgForm setDefaultPostFieldTransformer(Function1<String, String> fun) {
	this.defaultPostFieldTransformer = fun;
	return this;
    }

    public OdtField initializeField(OdtField field) {
	return getFieldInitializer(field.getName()).apply(field);
    }

    public Function1<OdtField, OdtField> getFieldInitializer(String fieldId) {
	return initializers.getOrElse(fieldId, getDefaultFieldInitializer());
    }

    public Function1<OdtField, OdtField> getDefaultFieldInitializer() {
	return defaultFieldInitializer;
    }

    public OdtSvgForm setDefaultFieldInitializer(Function1<OdtField, OdtField> fun) {
	defaultFieldInitializer = fun;
	return this;
    }

    public Function1<Properties, OdtField> getFieldGenerator(String fieldId) {
	return generators.getOrElse(fieldId, getDefaultFieldGenerator());
    }

    public Function1<Properties, OdtField> getDefaultFieldGenerator() {
	return defaultGenerator;
    }

    public OdtSvgForm putFieldGenerator(String fieldId, Function1<Properties, OdtField> fun) {
	generators.put(fieldId, fun);
	return this;
    }

    public OdtSvgForm setDefaultFieldGenerator(Function1<Properties, OdtField> fun) {
	defaultGenerator = fun;
	return this;
    }

    public OdtField generateField(Properties p) {
	String name = p.getProperty("name");
	return getFieldGenerator(name).apply(p);
    }

    /**
     * Set the path used to launch the external odt editor. This path will be
     * used to launch the program instead of Desktop.open
     *
     * @param command the path of the executable that can be used to open an odt
     * file
     */
    public OdtSvgForm setExternalEditorPath(StringExt command) {
	externalEditorPath = command;
	return this;
    }

    /**
     * Generates the fields
     *
     * @return this
     */
    public OdtSvgForm generateFields() {
	XmlDoc doc = XmlDoc.newInstance(svgFile.getFile());
	Iterable<XmlNode> descNodes = doc.getRoot().listChildren("desc", true);
	for (XmlNode node : descNodes) {
	    XmlNode parent = node.getParent();
	    String elementId = parent.getString("id");
	    String description = node.getContentAsString();
	    Properties properties = new StringExt(description).toProperties();
	    properties.setProperty("name", elementId);
	    OdtField field = generateField(properties);
	    field = initializeField(field);
	    fields.add(field);
	    field.setContext(this);
	    OdtFieldEditor editor = field.getEditor();
	    container.add(field.getEditor());
	}
	return this;
    }

    public OdtSvgForm onQuitForm(Runnable task) {
	onQuitFormCallbacks.add(task);
	return this;
    }

    /**
     * Called when the user presses the quit button
     */
    public void onQuitForm() {
	for (Runnable runnable : onQuitFormCallbacks) {
	    runnable.run();
	}
    }

    /**
     * Called when the user presses the save button
     */
    public void onSaveForm() {
	if (onSaveFormCallbacks.isEmpty()) {
	    FileExt doc = generateTempDocument();
	    JFileChooserExt fc = new JFileChooserExt().withFilter("Documento .odt", "odt");
	    FileExt file = fc.showCheckedSaveDialog(this, new String[]{".odt"});
	    if (file != null) {
		try {
		    Files.copy(doc.getFile().toPath(), file.getFile().toPath(), StandardCopyOption.REPLACE_EXISTING);
		} catch (IOException ex) {
		    Logger.getLogger(OdtSvgForm.class.getName()).log(Level.SEVERE, null, ex);
		}
	    } else {
		Logger.getLogger(OdtSvgForm.class.getName()).log(Level.INFO, "DO NOT OVERWRITE");
	    }
	} else {
	    for (Runnable runnable : onSaveFormCallbacks) {
		runnable.run();
	    }
	}
    }

    public OdtSvgForm onSaveForm(Runnable callback) {
	onSaveFormCallbacks.add(callback);
	return this;
    }

    /**
     * Called when the user presses the preview button
     */
    public void onPreviewForm() {
	if (!checkFields(fields)) {
	    return;
	}

	FileExt temp = generateTempDocument();
	if (temp != null) {
	    openFile(temp);
	}
    }

    public void openFile(FileExt odtFile) {
	if (externalEditorPath != null) {
	    odtFile.openWith(externalEditorPath.toString());
	} else {
	    try {
		Desktop.getDesktop().open(odtFile.getFile());
	    } catch (IOException ex) {
		Logger.getLogger(OdtSvgForm.class.getName()).log(Level.SEVERE, null, ex);
	    }
	}
    }

    /**
     * Called when the user presses the new button
     */
    public void onNewForm() {
	int op = JOptionPane.showConfirmDialog(this, "Cancellare il contenuto dei campi?", "Conferma", JOptionPane.YES_NO_OPTION);
	if (op == JOptionPane.YES_OPTION) {
	    for (OdtField field : fields) {
		field.clearEditedValue();
	    }
	}
    }

    /**
     * Called when the user presses the print button
     */
    public void onPrintForm() {
	if (!checkFields(fields)) {
	    return;
	}
	FileExt temp = generateTempDocument();
	if (temp != null) {
	    try {
		Desktop.getDesktop().print(temp.getFile());
	    } catch (IOException ex) {
		Logger.getLogger(OdtSvgForm.class.getName()).log(Level.SEVERE, null, ex);
	    }
	}
    }

    public Collection<OdtField> getFields() {
	return fields;
    }

    public void displayException(Exception ex) {
	JOptionPane.showMessageDialog(this, ex);
    }

    public void addMetaTransformer(Function1<OdtFiller, ?> transformer) {
	metaTransformers.add(transformer);
    }

    public FileExt generateTempDocument() {
	boolean checkPassed = true;

	for (Function1<OdtSvgForm, Boolean> check : fileGenerationCheck) {
	    checkPassed = check.apply(this);
	    if (!checkPassed) {
		break;
	    }
	}

	if (checkPassed) {
	    String xml = preTransformXml(getOdtContentXml());
	    for (OdtField field : fields) {
		xml = applyFieldTransform(field, xml);
	    }
	    xml = postFieldTransformXml(xml);

	    OdtFiller filler = new OdtFiller(fields);
	    for (Function1<OdtFiller, ?> function1 : metaTransformers) {
		function1.apply(filler);
	    }
	    String meta = filler.generateMeta(getOdtMetaXml());

	    Zipper zipper = new Zipper(file);
	    try {
		File temp = createTemporaryFile();
		if (meta != null) {
		    HashMap<String, byte[]> data = new HashMap<String, byte[]>();
		    data.put("content.xml", xml.getBytes("utf-8"));
		    data.put("meta.xml", meta.getBytes("utf-8"));
		    zipper.replace(temp, data);
		} else {
		    zipper.replace(temp, "content.xml", xml.getBytes(Charset.forName("utf-8")));
		}
		return new FileExt(temp);
	    } catch (IOException ex) {
		Logger.getLogger(OdtSvgForm.class.getName()).log(Level.INFO, "temp error", ex);
	    }
	}
	return null;
    }
    private final List<Function1<String, String>> postFieldTransformers = new LinkedList<Function1<String, String>>();

    public void addPostFieldTransformer(Function1<String, String> transformer) {
	postFieldTransformers.add(transformer);
    }

    public String postFieldTransformXml(String xml) {
	xml = defaultPostFieldTransformer.apply(xml);
	for (Function1<String, String> function1 : postFieldTransformers) {
	    xml = function1.apply(xml);
	}
	return xml;
    }

    /**
     * Generates a temporary file in the user home folder
     *
     * @return a new temporary odt file
     */
    public File createTemporaryFile() {
	String name = file.getFile().getName();
	String home = System.getProperty("user.home");
	File temp = new File(home, name);
	int index = 0;
	while (temp.exists()) {
	    temp = new File(home, index + name);
	    index++;
	}
	temp.deleteOnExit();
	return temp;
    }

    /**
     * Transforms the content.xml applying the values in the edited fields
     *
     * @param field the field to apply
     * @param xml the xml content to apply the field value on
     * @return the xml content transformed by the field
     */
    protected String applyFieldTransform(OdtField field, String xml) {
	String applierXml = defaultEditorApplier.apply(field, field.getEditor(), xml);
	if (applierXml == null) {
	    Tuple2<StringExt, String> fieldValue = field.getEditor().applyEditorValue(field, xml);
	    xml = fieldValue.getB();
	    StringExt value = fieldValue.getA();
	    String text = value == null ? "" : value.toString();
	    if (text == null) {
		text = "";
	    }
	    switch (field.getCase()) {
		case lower:
		    text = text.toLowerCase();
		    break;
		case upper:
		    text = text.toUpperCase();
		    break;
		case name:
		    if (text.length() > 0) {
			text = text.substring(0, 1).toUpperCase() + text.substring(1).toLowerCase();
		    }
		    break;
		case none:
		default:
		    break;
	    }
	    xml = xml.replace(field.getToken(), text);
	    xml = xml.replace(field.getToken().toLowerCase(), text);
	    xml = xml.replace(field.getToken().toUpperCase(), text);
	} else {
	    return applierXml;
	}
	return xml;
    }

    /**
     * Sets the optional function that applies the value of a field to the xml
     * document
     *
     * @param fun a function taking a field editor and the xml document and
     * returning the transformed xml document. If the function returns null the
     * the default transform will be applied. Otherwise the field will be
     * considered applied
     */
    public void setDefaultEditorApplier(Function3<OdtField, OdtFieldEditor, String, String> fun) {
	if (fun != null) {
	    defaultEditorApplier = fun;
	}
    }

    /*
     * field with type oplistitem. If the field is checked, remove the token of
     * the field if the field is not checked, remove the entire list item from
     * the content
     */
    protected String applyOpListItemTransform(OdtField field, String xml) {
	OdtOpListItemEditor editor = (OdtOpListItemEditor) field.getEditor();
	if (editor.isSelected()) {
	    xml = xml.replace(field.getToken(), "");
	} else {
	    int index = xml.indexOf(field.getToken());
	    int startIndex = 0;
	    StringBuilder buffer = new StringBuilder();
	    int lim = 50;
	    for (int i = index; i >= 0 && (--lim >= 0); i--) {
		buffer.insert(0, xml.charAt(i));
		if (buffer.indexOf("<text:list-item>") == 0) {
		    startIndex = i;
		    break;
		}
	    }
	    if (startIndex != 0) {
		String endToken = "</text:list-item>";
		int endIndex = xml.indexOf(endToken, startIndex);
		if (endIndex >= 0) {
		    xml = xml.substring(0, startIndex) + xml.substring(endIndex + endToken.length());
		}
	    }
	}
	return xml;
    }

    /**
     * Called before to apply the field replace procedure. This can be used to
     * transform a field into one or more fields. Does nothing here.
     *
     * @param xml the xml content to transform
     * @return the transformed xml content
     */
    public String preTransformXml(String xml) {
	return xml;
    }

    /**
     * Returns the text in the content.xml file of the odt document
     *
     * @return the content.xml file data
     */
    public String getOdtContentXml() {
	Zipper zipper = new Zipper(file);
	return zipper.getEntryAsText("content.xml", "utf-8");
    }

    public String getOdtMetaXml() {
	return new Zipper(file).getEntryAsText("meta.xml", "utf-8");
    }

    /**
     * Check the value of the edited fields
     *
     * @param fields the fields to check
     * @return true if the fields have valid data, false otherwise
     */
    public boolean checkFields(Collection<OdtField> fields) {
	return true;
    }
    /**
     * Returns the field with the given name
     *
     * @param fieldName the name of the field to return
     * @return the requested field or null if no such field exists
     */
    private OdtField cachedField;

    public OdtField getField(final String fieldName) {
	if (cachedField != null && cachedField.getName().equals(fieldName)) {
	    return cachedField;
	}

	return fields.find(new Function1<OdtField, Boolean>() {

	    public Boolean apply(OdtField p) {
		return fieldName.equalsIgnoreCase(p.getName());
	    }
	}).getOrElse(0, null);
    }

    public boolean hasField(String name) {
	cachedField = getField(name);
	return cachedField != null;
    }

    public static void main(String[] args) {
	java.awt.EventQueue.invokeLater(new Runnable() {

	    @Override
	    public void run() {
		JFrame f = new JFrame("Test");
		f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
		f.add(new OdtSvgForm(FileExt.newUserFolderFile("documents/test.odt")).setExternalEditorPath(new StringExt("c:\\program files (x86)\\libreoffice 3\\program\\swriter.exe")).generateFields());
		f.setSize(800, 600);
		f.setVisible(true);
	    }
	});
    }

    public OdtSvgForm setFieldValue(String... nameValuePairs) {
	for (int i = 0; i < nameValuePairs.length; i += 2) {
	    String name = nameValuePairs[i];
	    String value = nameValuePairs[i + 1];
	    getField(name).getEditor().setValue(StringExt.wrap(value));
	}
	return this;
    }

    /**
     * Find a field and set the value of its editor to the given one. Logs errors
     */
    public void setEditorValue(String fieldName, Object value) {
	OdtField field = getField(fieldName);
	if (field != null) {
	    OdtFieldEditor editor = field.getEditor();
	    if (editor != null) {
		editor.setValue(StringExt.wrap(value));
	    } else {
		Log.message("Field ", fieldName, " has no editor.");
	    }
	} else {
	    Log.message("Form can't find field named: ", fieldName);
	}
    }

    public String deleteParagraph(String xml, String tokenInParagraph) {
	return OdtParagraph.deleteParagraph(xml, tokenInParagraph);
    }
}
